/***
 * Copyright (c) 2009 Caelum - www.caelum.com.br/opensource All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package br.com.caelum.vraptor.scan;

import static com.google.common.base.Objects.firstNonNull;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.scannotation.AnnotationDB;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import br.com.caelum.vraptor.core.BaseComponents;
import br.com.caelum.vraptor.ioc.Stereotype;

/**
 * A Scannotation based Component Scanner
 *
 * @author Sérgio Lopes
 * @since 3.2
 */
public class ScannotationComponentScanner implements ComponentScanner {

	private static final Logger logger = LoggerFactory.getLogger(ScannotationComponentScanner.class);

	public Collection<String> scan(ClasspathResolver resolver) {
	final URL webInfClasses = resolver.findWebInfClassesLocation();
	final List<String> basePackages = resolver.findBasePackages();

	HashSet<String> results = new HashSet<String>();

	Map<String, Set<String>> webInfClassesAnnotationMap = scanWebInfClasses(webInfClasses);
	Map<String, Set<String>> basePackagesAnnotationMap = scanBasePackages(basePackages, resolver);

	Set<String> stereotypeNames = findStereotypes(webInfClassesAnnotationMap, basePackagesAnnotationMap, basePackages);

	findComponentsFromWebInfClasses(webInfClassesAnnotationMap, stereotypeNames, results);
	findComponentsFromBasePackages(basePackagesAnnotationMap, basePackages, results);

	return results;
	}

	private Map<String, Set<String>> scanWebInfClasses(URL webInfClasses) {
	try {
		AnnotationDB db = createAnnotationDB();
		db.scanArchives(webInfClasses);
		return db.getAnnotationIndex();
	} catch (FileNotFoundException e) {
		return Collections.emptyMap();
	} catch (IOException e) {
		throw new ScannerException("Could not scan WEB-INF/classes", e);
	}
	}

	private Map<String, Set<String>> scanBasePackages(List<String> basePackages, ClasspathResolver resolver) {
	try {
		AnnotationDB db = createAnnotationDB();

		for (String basePackage : basePackages) {
		scanPackage(basePackage, db, resolver);
		}

		return db.getAnnotationIndex();
	} catch (IOException e) {
		throw new ScannerException("Could not scan base packages", e);
	}
	}



	private void scanPackage(String basePackage, AnnotationDB db, ClasspathResolver resolver) throws IOException {
	String resource = basePackage.replace('.', '/');
	Enumeration<URL> urls = resolver.getClassLoader().getResources(resource);
	if (!urls.hasMoreElements()) {
		logger.error("There's no occurence of package {} in classpath", basePackage);
		return;
	}
	do {
		URL url = urls.nextElement();

		String file = toFileName(resource, url);

		db.scanArchives(new URL(file));
	} while (urls.hasMoreElements());
	}

	private static String toFileName(String resource, URL url) {
	String file = url.getFile().substring(0, url.getFile().length() - resource.length() - 1).replaceAll("(!)(/)?$", "");

	if (!file.startsWith("file:")) {
		file = "file:" + file;
	}
	return file;
	}

	private Set<String> findStereotypes(Map<String, Set<String>> webInfClassesAnnotationMap, Map<String, Set<String>> basePackagesAnnotationMap, List<String> basePackages) {
	HashSet<String> results = new HashSet<String>();

	addVRaptorStereotypes(results);

	addWebInfClassesStereotypes(webInfClassesAnnotationMap, results);

	addBasePackagesStereotypes(basePackagesAnnotationMap, basePackages, results);

	return results;
	}

	private void addBasePackagesStereotypes(Map<String, Set<String>> basePackagesAnnotationMap,
		List<String> basePackages, HashSet<String> results) {
	Set<String> libStereotypes = nullToEmpty(basePackagesAnnotationMap.get(Stereotype.class.getName()));
	for (String stereotype : libStereotypes) {
		if (packagesContains(basePackages, stereotype)) {
		results.add(stereotype);
		}
	}
	}

	private static boolean packagesContains(List<String> basePackages, String clazz) {
	for (String basePackage : basePackages) {
		if (clazz.startsWith(basePackage)) {
		return true;
		}
	}
	return false;
	}

	private void addWebInfClassesStereotypes(Map<String, Set<String>> webInfClassesAnnotationMap,
		HashSet<String> results) {
	Set<String> myStereotypes = nullToEmpty(webInfClassesAnnotationMap.get(Stereotype.class.getName()));
	results.addAll(myStereotypes);
	}

	private static void addVRaptorStereotypes(HashSet<String> results) {
	for (Class<? extends Annotation> stereotype : BaseComponents.getStereotypes()) {
		results.add(stereotype.getName());
	}
	}

	private void findComponentsFromWebInfClasses(Map<String, Set<String>> index, Set<String> stereotypeNames, Set<String> results) {
	for (String stereotype : stereotypeNames) {
		Set<String> classes = nullToEmpty(index.get(stereotype));
		results.addAll(classes);
	}
	}

	private void findComponentsFromBasePackages(Map<String, Set<String>> index, List<String> basePackages, Set<String> results) {
	for (Class<? extends Annotation> stereotype : BaseComponents.getStereotypes()) {
		Set<String> classes = nullToEmpty(index.get(stereotype.getName()));

		for (String clazz : classes) {
		if (packagesContains(basePackages, clazz)) {
			results.add(clazz);
		}
		}
	}
	}

	private <T> Set<T> nullToEmpty(Set<T> set) {
	return firstNonNull(set, Collections.<T>emptySet());
	}

	private AnnotationDB createAnnotationDB() {
	AnnotationDB db = new AnnotationDB();
	db.setScanClassAnnotations(true);
	db.setScanFieldAnnotations(false);
	db.setScanMethodAnnotations(false);
	db.setScanParameterAnnotations(false);
	return db;
	}
}